
function ResourceEventRenderer() {
    var t = this;
	
	
    // exports
    t.renderEvents = renderEvents;
    t.compileDaySegs = compileDaySegs; // for DayEventRenderer
    t.clearEvents = clearEvents;
    t.slotSegHtml = slotSegHtml;
    t.bindDaySeg = bindDaySeg;
	
    
    // imports
    DayEventRenderer.call(t);
    var opt = t.opt;
    var trigger = t.trigger;
    //var setOverflowHidden = t.setOverflowHidden;
    var isEventDraggable = t.isEventDraggable;
    var isEventResizable = t.isEventResizable;
    var eventEnd = t.eventEnd;
    var reportEvents = t.reportEvents;
    var reportEventClear = t.reportEventClear;
    var eventElementHandlers = t.eventElementHandlers;
    var setHeight = t.setHeight;
    var getDaySegmentContainer = t.getDaySegmentContainer;
    var getSlotSegmentContainer = t.getSlotSegmentContainer;
    var getHoverListener = t.getHoverListener;
    var getMaxMinute = t.getMaxMinute;
    var getMinMinute = t.getMinMinute;
    var timePosition = t.timePosition;
    var colContentLeft = t.colContentLeft;
    var colContentRight = t.colContentRight;
    var renderDaySegs = t.renderDaySegs;
    var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture
    var getColCnt = t.getColCnt;
    var getColWidth = t.getColWidth;
    var getSlotHeight = t.getSlotHeight;
    var getBodyContent = t.getBodyContent;    
    var reportEventElement = t.reportEventElement;
    var showEvents = t.showEvents;
    var hideEvents = t.hideEvents;
    var eventDrop = t.eventDrop;
    var eventResize = t.eventResize;
    var renderDayOverlay = t.renderDayOverlay;
    var clearOverlays = t.clearOverlays;
    var calendar = t.calendar;
    var formatDate = calendar.formatDate;
    var formatDates = calendar.formatDates;
    var resourceCol = t.resourceCol;  // imported from ResourceView.js
    var resources = t.resources;  // imported from ResourceView.js
	
	
	
    /* Rendering
	----------------------------------------------------------------------------*/
	

    function renderEvents(events, modifiedEventId) {
        reportEvents(events);
        var i, len=events.length,
        dayEvents=[],
        slotEvents=[];
        for (i=0; i<len; i++) {
            if (events[i].allDay) {
                dayEvents.push(events[i]);
            }else{
                slotEvents.push(events[i]);
            }
        }
        if (opt('allDaySlot')) {
            renderDaySegs(compileDaySegs(dayEvents), modifiedEventId, resources);
            setHeight(); // no params means set to viewHeight
        }
        renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
    }
	
	
    function clearEvents() {
        reportEventClear();
        getDaySegmentContainer().empty();
        getSlotSegmentContainer().empty();
    }
	
    
    function compileDaySegs(events) {
        var levels = stackSegs(sliceSegs(events, $.map(events, exclEndDay), t.visStart, t.visEnd)),
        i, levelCnt=levels.length, level,
        j, seg,
        segs=[];
        for (i=0; i<levelCnt; i++) {
            level = levels[i];
            for (j=0; j<level.length; j++) {
                seg = level[j];
                seg.row = 0;
                seg.level = i; // not needed anymore
                segs.push(seg);
            }
        }
        return segs;
    }
	
    
    function compileSlotSegs(events) {
        var colCnt = getColCnt(),
        minMinute = getMinMinute(),
        maxMinute = getMaxMinute(),
        d = addMinutes(cloneDate(t.visStart), minMinute),
        i, col,
        j, level,
        k, seg,
        segs=[];
        for (i=0; i<colCnt; i++) {
            // only events for this resource        
            var resourceEvents = eventsForResource(resources[i], events);
                    
            col = stackSegs(sliceSegs(resourceEvents, $.map(resourceEvents, slotEventEnd), d, addMinutes(cloneDate(d), maxMinute-minMinute)));
            countForwardSegs(col);
            for (j=0; j<col.length; j++) {
                level = col[j];
                for (k=0; k<level.length; k++) {
                    seg = level[k];
                    seg.col = i;
                    seg.level = j;
                    segs.push(seg);
                }
            }
        }
        return segs;
    }
        
        
    function eventsForResource(resource, events) {
        var resourceEvents = [];
		
        for(var i=0; i<events.length; i++) {
            if(events[i].resourceId === resource.id) {
                resourceEvents.push(events[i])
            }
        }
		
        return resourceEvents;
    }
	
	
    function slotEventEnd(event) {
        if (event.end) {
            return cloneDate(event.end);
        }else{
            return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
        }
    }
	
    
    // renders events in the 'time slots' at the bottom
	
    function renderSlotSegs(segs, modifiedEventId) {
	
        var i, segCnt=segs.length, seg,
        event,
        classes,
        top, bottom,
        colI, levelI, forward,
        leftmost,
        availWidth,
        outerWidth,
        left,
        html='',
        eventElements,
        eventElement,
        triggerRes,
        vsideCache={},
        hsideCache={},
        key, val,
        contentElement,
        height,
        slotSegmentContainer = getSlotSegmentContainer(),
        rtl, dis, dit,
        colCnt = getColCnt();
			
        if (rtl = opt('isRTL')) {
            dis = -1;
            dit = colCnt - 1;
        }else{
            dis = 1;
            dit = 0;
        }
			
        // calculate position/dimensions, create html
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            event = seg.event;
            top = timePosition(seg.start, seg.start);
            bottom = timePosition(seg.start, seg.end);
            colI = seg.col;
            levelI = seg.level;
            forward = seg.forward || 0;
            leftmost = colContentLeft(colI*dis + dit);
            availWidth = colContentRight(colI*dis + dit) - leftmost;
            availWidth = Math.min(availWidth-6, availWidth*.95); // TODO: move this to CSS
            if (levelI) {
                // indented and thin
                outerWidth = availWidth / (levelI + forward + 1);
            }else{
                if (forward) {
                    // moderately wide, aligned left still
                    outerWidth = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
                }else{
                    // can be entire width, aligned left
                    outerWidth = availWidth;
                }
            }
            left = leftmost +                                  // leftmost possible
            (availWidth / (levelI + forward + 1) * levelI) // indentation
            * dis + (rtl ? availWidth - outerWidth : 0);   // rtl
            seg.top = top;
            seg.left = left;
            seg.outerWidth = outerWidth;
            seg.outerHeight = bottom - top;
            html += slotSegHtml(event, seg);
        }
        slotSegmentContainer[0].innerHTML = html; // faster than html()
        eventElements = slotSegmentContainer.children();
		
        // retrieve elements, run through eventRender callback, bind event handlers
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            event = seg.event;
            eventElement = $(eventElements[i]); // faster than eq()
            triggerRes = trigger('eventRender', event, event, eventElement);
            if (triggerRes === false) {
                eventElement.remove();
            }else{
                if (triggerRes && triggerRes !== true) {
                    eventElement.remove();
                    eventElement = $(triggerRes)
                    .css({
                        position: 'absolute',
                        top: seg.top,
                        left: seg.left
                    })
                    .appendTo(slotSegmentContainer);
                }
                seg.element = eventElement;
                if (event._id === modifiedEventId) {
                    bindSlotSeg(event, eventElement, seg);
                }else{
                    eventElement[0]._fci = i; // for lazySegBind
                }
                reportEventElement(event, eventElement);
            }
        }
		
        lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
		
        // record event sides and title positions
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            if (eventElement = seg.element) {
                val = vsideCache[key = seg.key = cssKey(eventElement[0])];
                seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement, true)) : val;
                val = hsideCache[key];
                seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement, true)) : val;
                contentElement = eventElement.find('div.fc-event-content');
                if (contentElement.length) {
                    seg.contentTop = contentElement[0].offsetTop;
                }
            }
        }
		
        // set all positions/dimensions at once
        for (i=0; i<segCnt; i++) {
            seg = segs[i];
            if (eventElement = seg.element) {
                eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
                height = Math.max(0, seg.outerHeight - seg.vsides);
                eventElement[0].style.height = height + 'px';
                event = seg.event;
                if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
                    // not enough room for title, put it in the time header
                    eventElement.find('div.fc-event-time')
                    .text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
                    eventElement.find('div.fc-event-title')
                    .remove();
                }
                trigger('eventAfterRender', event, event, eventElement);
            }
        }
					
    }
	
	
    function slotSegHtml(event, seg) {
        var html = "<";
        var url = event.url;
        var skinCss = getSkinCss(event, opt);
        var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
        var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
        if (isEventDraggable(event)) {
            classes.push('fc-event-draggable');
        }
        if (seg.isStart) {
            classes.push('fc-corner-top');
        }
        if (seg.isEnd) {
            classes.push('fc-corner-bottom');
        }
        classes = classes.concat(event.className);
        if (event.source) {
            classes = classes.concat(event.source.className || []);
        }
        if (url) {
            html += "a href='" + htmlEscape(event.url) + "'";
        }else{
            html += "div";
        }
        html +=
        " class='" + classes.join(' ') + "'" +
        " style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px;" + skinCss + "'" +
        ">" +
        "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
        "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
        "<div class='fc-event-time'>" +
        htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
        "</div>" +
        "</div>" +
        "<div class='fc-event-content'>" +
        "<div class='fc-event-title'>" +
        htmlEscape(event.title) +
        "</div>" +
        "</div>" +
        "<div class='fc-event-bg'></div>" +
        "</div>"; // close inner
        if (seg.isEnd && isEventResizable(event)) {
            html +=
            "<div class='ui-resizable-handle ui-resizable-s'>=</div>";
        }
        html +=
        "</" + (url ? "a" : "div") + ">";
        return html;
    }
    

    function bindDaySeg(event, eventElement, seg) {
        if (isEventDraggable(event)) {
            draggableDayEvent(event, eventElement, seg.isStart);
        }
        if (seg.isEnd && isEventResizable(event)) {
            resizableDayEvent(event, eventElement, seg);
        }
        eventElementHandlers(event, eventElement);
    // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
    }
	
	
    function bindSlotSeg(event, eventElement, seg) {
        var timeElement = eventElement.find('div.fc-event-time');
        if (isEventDraggable(event)) {
            draggableSlotEvent(event, eventElement, timeElement);
        }
        if (seg.isEnd && isEventResizable(event)) {
            resizableSlotEvent(event, eventElement, timeElement);
        }
        eventElementHandlers(event, eventElement);
    }
	
	
	
    /* Dragging
	-----------------------------------------------------------------------------------*/
	
	
    // when event starts out FULL-DAY
	
    function draggableDayEvent(event, eventElement, isStart) {
        var origWidth;
        var revert;
        var allDay=true;
        var dayDelta;
        var dis = opt('isRTL') ? -1 : 1;
        var hoverListener = getHoverListener();
        var colWidth = getColWidth();
        var slotHeight = getSlotHeight();
        var minMinute = getMinMinute();
        eventElement.draggable({
            zIndex: 9,
            opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
            revertDuration: opt('dragRevertDuration'),
            start: function(ev, ui) {
                trigger('eventDragStart', eventElement, event, ev, ui);
                hideEvents(event, eventElement);
                origWidth = eventElement.width();
                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                    clearOverlays();
                    if (cell) {
                        //setOverflowHidden(true);
                        revert = false;
                        dayDelta = colDelta * dis;
                        if (!cell.row) {
                            // on full-days
                            renderDayOverlay(
                                addDays(cloneDate(event.start), dayDelta),
                                addDays(exclEndDay(event), dayDelta),
                                true,
                                resources[cell.col]
                                );
                            resetElement();
                        }else{
                            // mouse is over bottom slots
                            if (isStart) {
                                if (allDay) {
                                    // convert event to temporary slot-event
                                    eventElement.width(colWidth - 10); // don't use entire width
                                    setOuterHeight(
                                        eventElement,
                                        slotHeight * Math.round(
                                            (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes'))
                                            / opt('slotMinutes')
                                            )
                                        );
                                    eventElement.draggable('option', 'grid', [colWidth, 1]);
                                    allDay = false;
                                }
                            }else{
                                revert = true;
                            }
                        }
                        revert = revert || (allDay && !dayDelta);
                    }else{
                        resetElement();
                        //setOverflowHidden(false);
                        revert = true;
                    }
                    eventElement.draggable('option', 'revert', revert);
                }, ev, 'drag');
            },
            stop: function(ev, ui) {
                var cell = hoverListener.stop();
                clearOverlays();
                event.resourceId = resources[cell.col].id;
                trigger('eventDragStop', eventElement, event, ev, ui);
                if (revert) {
                    // hasn't moved or is out of bounds (draggable has already reverted)
                    resetElement();
                    eventElement.css('filter', ''); // clear IE opacity side-effects
                    showEvents(event, eventElement);
                }else{
                    // changed!
                    var minuteDelta = 0;
                    if (!allDay) {
                        minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight)
                        * opt('slotMinutes')
                        + minMinute
                        - (event.start.getHours() * 60 + event.start.getMinutes());
                    }
                    
                    dayDelta = 0;
                    eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
                }
            //setOverflowHidden(false);
            }
        });
        function resetElement() {
            if (!allDay) {
                eventElement
                .width(origWidth)
                .height('')
                .draggable('option', 'grid', null);
                allDay = true;
            }
        }
    }
	
	
    // when event starts out IN TIMESLOTS
	
    function draggableSlotEvent(event, eventElement, timeElement) {
        var origPosition;
        var allDay=false;
        var dayDelta;
        var minuteDelta;
        var prevMinuteDelta;
        var dis = opt('isRTL') ? -1 : 1;
        var hoverListener = getHoverListener();
        var colCnt = getColCnt();
        var colWidth = getColWidth();
        var slotHeight = getSlotHeight();
        eventElement.draggable({
            zIndex: 9,
            scroll: false,
            grid: [colWidth, slotHeight],
            axis: colCnt==1 ? 'y' : false,
            opacity: opt('dragOpacity'),
            revertDuration: opt('dragRevertDuration'),
            start: function(ev, ui) {
                trigger('eventDragStart', eventElement, event, ev, ui);
                hideEvents(event, eventElement);
                origPosition = eventElement.position();
                minuteDelta = prevMinuteDelta = 0;
                hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
                    eventElement.draggable('option', 'revert', !cell);
                    clearOverlays();
                    if (cell) {
                        dayDelta = colDelta * dis;
                        if (opt('allDaySlot') && !cell.row) {
                            // over full days
                            if (!allDay) {
                                // convert to temporary all-day event
                                allDay = true;
                                timeElement.hide();
                                eventElement.draggable('option', 'grid', null);
                            }
                            renderDayOverlay(
                                addDays(cloneDate(event.start), dayDelta),
                                addDays(exclEndDay(event), dayDelta),
                                true,
                                resources[cell.col]
                                );
                        }else{
                            // on slots
                            resetElement();
                        }
                    }
                }, ev, 'drag');
            },
            drag: function(ev, ui) {
                minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes');
                if (minuteDelta != prevMinuteDelta) {
                    if (!allDay) {
                        updateTimeText(minuteDelta);
                    }
                    prevMinuteDelta = minuteDelta;
                }
            },
            stop: function(ev, ui) {
                var cell = hoverListener.stop();
                clearOverlays();
                trigger('eventDragStop', eventElement, event, ev, ui);
                if (cell && (dayDelta || minuteDelta || allDay)) {
                    // changed!
                    event.oldResourceId = event.resourceId;
                    event.resourceId = resources[cell.col].id;
                    dayDelta = 0;
                    eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui);
                }else{
                    // either no change or out-of-bounds (draggable has already reverted)
                    resetElement();
                    eventElement.css('filter', ''); // clear IE opacity side-effects
                    eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
                    updateTimeText(0);
                    showEvents(event, eventElement);
                }
            }
        });
        function updateTimeText(minuteDelta) {
            var newStart = addMinutes(cloneDate(event.start), minuteDelta);
            var newEnd;
            if (event.end) {
                newEnd = addMinutes(cloneDate(event.end), minuteDelta);
            }
            timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
        }
        function resetElement() {
            // convert back to original slot-event
            if (allDay) {
                timeElement.css('display', ''); // show() was causing display=inline
                eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
                allDay = false;
            }
        }
    }
	
	
	
    /* Resizing
	--------------------------------------------------------------------------------------*/
	
	
    function resizableSlotEvent(event, eventElement, timeElement) {
        var slotDelta, prevSlotDelta;
        var slotHeight = getSlotHeight();
        eventElement.resizable({
            handles: {
                s: 'div.ui-resizable-s'
            },
            grid: slotHeight,
            start: function(ev, ui) {
                slotDelta = prevSlotDelta = 0;
                hideEvents(event, eventElement);
                eventElement.css('z-index', 9);
                trigger('eventResizeStart', this, event, ev, ui);
            },
            resize: function(ev, ui) {
                // don't rely on ui.size.height, doesn't take grid into account
                slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight);
                if (slotDelta != prevSlotDelta) {
                    timeElement.text(
                        formatDates(
                            event.start,
                            (!slotDelta && !event.end) ? null : // no change, so don't display time range
                            addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta),
                            opt('timeFormat')
                            )
                        );
                    prevSlotDelta = slotDelta;
                }
            },
            stop: function(ev, ui) {
                trigger('eventResizeStop', this, event, ev, ui);
                if (slotDelta) {
                    eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui);
                }else{
                    eventElement.css('z-index', 8);
                    showEvents(event, eventElement);
                // BUG: if event was really short, need to put title back in span
                }
            }
        });
    }
	

}